/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2010 huliqing, huliqing.cn@gmail.com
 *
 * This file is part of QFaces.
 * QFaces is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * QFaces is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with QFaces.  If not, see <http://www.gnu.org/licenses/>.
 *
 * 这个文件是QFaces的一部分。
 * 您可以单独使用或分发这个文件，但请不要移除这个头部声明信息.
 * QFaces是一个自由软件，您可以自由分发、修改其中的源代码或者重新发布它，
 * 新的任何修改后的重新发布版必须同样在遵守LGPL3或更后续的版本协议下发布.
 * 关于LGPL协议的细则请参考COPYING、COPYING.LESSER文件，
 * 您可以在QFaces的相关目录中获得LGPL协议的副本，
 * 如果没有找到，请连接到 http://www.gnu.org/licenses/ 查看并获取副本。
 *
 * - Author: Huliqing
 * - Contact: huliqing.cn@gmail.com
 * - License: GNU Lesser General Public License (LGPL)
 * - Blog and source code availability: http://www.huliqing.name/
 */


package name.huliqing.qfaces.component;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.convert.Converter;
import javax.servlet.http.HttpServletRequest;
import name.huliqing.qfaces.QFaces;
import name.huliqing.qfaces.model.Item;

/**
 *
 * @author huliqing
 */
public class Editor extends UIInput {

    private final static Logger logger = Logger.getLogger(Editor.class.getName());
    // -- 组件属性参数
    private String fontNameList;	// 字体参数,用逗号分格多个字体:  宋体,隶书,TimeNew Roman
    private String fontSizeList;        // 字体大小
    private String colorList;           // 颜色参数，用逗号分格多个颜色：FFFFFF,000000,123456,
    private String styleClass;          // 最外框样式类
    private String style;               // 最外框的样式
    private String innerStyle;          // 内框样式
    private String toolsStyle;          // Tools区域的样式
    private String toolsStyleClass;     // Tools区域的样式类
    private Boolean enableInsertCode;	// 是否开启"插入代码"的功能,这个功能不是很常用，但是需要太多的JS分析代码，所以默认不打开
    private int mode = 1;          		// 1：normal模式；  0: source模式
    
    // ID属性, 重要，所有ID值不要保存状态属性，
    // 在渲染及解码过程中所有的ID都可能动态变动，这很重要，
    // 特别是在DataTable中。
    private String ID_CLIENT;           // 客户端ID，即clientId
    private String ID_FULL_ZONE;        // 全屏区域
    private String ID_MIN_ZONE;			// 正常区域
    private String ID_VIEW_ZONE;	 	// 组件的可视区域，包含 input,frame,tools等,切换于 Full,Min Zone之间
    private String ID_INPUT;            // Input 框的ID
    private String ID_IFRAME;           // IFrame框的ID
    private String ID_CONTENT;			// 编辑区域的ID
    private String ID_MODE;             // Mode 框的ID，该Field 用于记住当前编辑器所在的模式的值.
    private String ID_TOOLS;            // 工具栏的ID（不包含“全屏”按钮、“ViewSource”按钮）
    private String ID_TOOLS_EXT;        // 工具栏:“全屏”按钮、“ViewSource”按钮
    private String ID_ZOOM_IN;			// “放大”按钮ID
    private String ID_ZOOM_OUT;

    public Editor() {
        super();
        setRendererType(null);
    }

    @Override
    public void encodeBegin(FacesContext fc) throws IOException {
        // 获取参数值
        style = (String) this.getAttributes().get("style");
        styleClass = (String) this.getAttributes().get("styleClass");
        toolsStyle = (String) this.getAttributes().get("toolsStyle");
        toolsStyleClass = (String) this.getAttributes().get("toolsStyleClass");
        fontNameList = (String) this.getAttributes().get("fontNameList");
        fontSizeList = (String) this.getAttributes().get("fontSizeList");
        colorList = (String) this.getAttributes().get("colorList");
        enableInsertCode = Boolean.valueOf((String) this.getAttributes().get("enableInsertCode"));

        // 设置默认参数值
        if (fontNameList == null) {
            fontNameList = "TimeNew Roman,Arial,Impact,Tahoma,宋体,隶书,楷体,黑体";
        }

        if (fontSizeList == null) {
            fontSizeList = "2,4,6,8,10,12,14,16,18,20,22,24";
        }

        if (colorList == null) {
            colorList = "red,green,blue,orange,pink,white,black";
        }

        if (style == null) {
            style = "width:500px;padding:0px;background:white;";
        }

        if (toolsStyle == null) {
            toolsStyle = "background:#F0F0F0;width:100%;";
        }

        // 这个属性不应该开放给用户
        innerStyle = "width:100%;height:100%;background:white;border:0px solid #C0C0C0;padding:0px;";
        if (enableInsertCode == null) {
            enableInsertCode = false;
        }

        ID_CLIENT = getClientId(fc);
        // 对于Input组件，应该尽量保持clientId与自定义渲染的inputID一置
        ID_INPUT = ID_CLIENT;
        ID_IFRAME = ID_CLIENT + ":iframe";
        ID_CONTENT = ID_CLIENT + ":content";
        // 区域:放大，缩小
        ID_FULL_ZONE = ID_CLIENT + ":fullZone";
        ID_MIN_ZONE = ID_CLIENT + ":minZone";
        // 可视区域，包含于 FULL_ZONE/MIN_ZONE中
        ID_VIEW_ZONE = ID_CLIENT + ":view";
        // 模式
        ID_MODE = ID_CLIENT + ":mode";
        ID_TOOLS = ID_CLIENT + ":tools";
        ID_TOOLS_EXT = ID_CLIENT + ":toolsExt";
        ID_ZOOM_IN = ID_CLIENT + ":zoomIn";
        ID_ZOOM_OUT = ID_CLIENT + ":zoomOut";
        

        // ---- 载入必须的资源
        QFaces.getResLoader2().loadJavaScript(this, "Editor.js");

        // 如果打开了该功能，则需要装载代码分析用的js,该功能并不是太常见，所以默认关闭，可以减少JS代码的装载以提高性能
        if (this.enableInsertCode) {
            QFaces.getResLoader2().loadJavaScript(this, "Editor-InsertCode.js");
        }

        // ---- 开始渲染组件

        // 全屏区域
        ResponseWriter rw = fc.getResponseWriter();
        rw.startElement("div", this);
        rw.writeAttribute("id", ID_FULL_ZONE, null);
        rw.writeAttribute("style", "width:100%;height:93%;position:absolute;top:0;left:0;", null);
        rw.endElement("div");

        // 组件主体
        rw.startElement("div", this);
        rw.writeAttribute("class", styleClass, null);
        rw.writeAttribute("style", style, null);
        rw.writeAttribute("id", ID_MIN_ZONE, null);
	        rw.startElement("table", this);
	        rw.writeAttribute("id", ID_VIEW_ZONE, null);
	        rw.writeAttribute("border", "0", null);
	        rw.writeAttribute("cellspacing", "0", null);
	        rw.writeAttribute("cellpadding", "0", null);
	        rw.writeAttribute("style", innerStyle, null);
	        rw.startElement("tr", this);
		        rw.startElement("td", this);
		        rw.writeAttribute("height", "30", null);
		        renderTop(fc, initTools(fc, this.getClientId(fc)));
		        rw.endElement("td");
	        rw.endElement("tr");
	
	        rw.startElement("tr", this);
		        rw.startElement("td", this);
		        rw.writeAttribute("id", ID_CONTENT, null);
		        renderContent(fc);
		        rw.endElement("td");
	        rw.endElement("tr");
	        rw.endElement("table");
        rw.endElement("div");

        // - 保存隐藏变量,用于记住submit之前mode,
        rw.startElement("input", this);
        rw.writeAttribute("type", "hidden", null);
        rw.writeAttribute("id", this.ID_MODE, null);
        rw.writeAttribute("name", this.ID_MODE, null);
        rw.writeAttribute("value", this.mode, null);
        rw.endElement("input");

        // init
        StringBuilder jsInit = new StringBuilder("Q.F.UIEditor.initEditor(")
        .append("'" + this.ID_CLIENT + "'")
        .append(",'" + this.ID_MIN_ZONE + "'")
        .append(",'" + this.ID_FULL_ZONE + "'")
        .append(",'" + this.ID_VIEW_ZONE + "'")
        .append(",'" + this.ID_INPUT + "'")
        .append(",'" + this.ID_IFRAME + "'")
        .append(",'" + this.ID_CONTENT + "'")
        .append(",'" + this.ID_MODE + "'")
        .append(",'" + this.ID_TOOLS + "'")
        .append(",'" + this.ID_TOOLS_EXT + "'")
        .append(",'" + this.ID_ZOOM_IN + "'")
        .append(",'" + this.ID_ZOOM_OUT + "'")
        .append(",'" + this.colorList + "'")
        .append(")");

        QFaces.getResLoader2().loadJavaScriptInit(this, jsInit.toString());
    }

    private void renderTop(FacesContext fc, List<Tool> tools) throws IOException {
        ResponseWriter rw = fc.getResponseWriter();
        rw.startElement("table", this);
        rw.writeAttribute("style", toolsStyle, null);
        rw.writeAttribute("class", toolsStyleClass, null);
        rw.writeAttribute("border", "0", null);
        rw.startElement("tr", this);
        rw.startElement("td", this);
        rw.startElement("div", this);
        rw.writeAttribute("id", ID_TOOLS, null);
        rw.writeAttribute("name", ID_TOOLS, null);
        renderTools(fc, tools);
        rw.endElement("div");
        rw.endElement("td");
        rw.startElement("td", this);
        rw.writeAttribute("width", "60", null);
        rw.writeAttribute("valign", "top", null);
        rw.startElement("div", this);
        rw.writeAttribute("id", ID_TOOLS_EXT, null);
        rw.writeAttribute("name", ID_TOOLS_EXT, null);
        renderToolsExt(fc);
        rw.endElement("div");
        rw.endElement("td");
        rw.endElement("tr");
        rw.endElement("table");
    }

    public void renderContent(FacesContext fc) throws IOException {
        // 获取数据，并进行可能的转换
        Object _valueObj = this.getValue();
        String _value = null;
        if (_valueObj != null) {
            Converter conv = this.getConverter();
            if (conv != null) {
                _value = conv.getAsString(fc, this, _valueObj);
            } else {
                _value = _valueObj.toString();
            }
        }

        ResponseWriter rw = fc.getResponseWriter();
        rw.startElement("textarea", this);
        rw.writeAttribute("id", this.ID_INPUT, null);
        rw.writeAttribute("name", this.ID_INPUT, null);
//        rw.writeAttribute("wrap", "virtual", null); // wrap, virtual, physical(自动换行)
        rw.writeAttribute("style", "width:99.5%;background:#FFF;", null);
        rw.writeText(_value != null ? _value : "", null);
        rw.endElement("textarea");

        rw.startElement("iframe", this);
        rw.writeAttribute("id", this.ID_IFRAME, null);
        rw.writeAttribute("name", this.ID_IFRAME, null);
        rw.writeAttribute("marginWidth", "0", null);
        rw.writeAttribute("marginHeight", "0", null);
        rw.writeAttribute("frameSpacing", "0", null);
        rw.writeAttribute("style", "display:none;width:99.8%;background:#FFF;", null);
        rw.writeAttribute("frameborder", "0", null);
        rw.writeAttribute("scrolling", "auto", null);
        rw.endElement("iframe");
    }

    private void renderTools(FacesContext fc, List<Tool> tools) throws IOException {
        for (Tool tool : tools) {
            tool.encode();
        }
    }

    private void renderToolsExt(FacesContext fc) throws IOException {
        new ImageTool("Q.F.UIEditor.swapMode('" + getClientId(fc) + "')", "编辑HTML", "09-source.png").encode();
        //new ImageTool("Q.F.UIEditor.zoom('" + getClientId(fc) + "')", "全屏", "09-zoom-in.gif").encode();
        new FullScreenTool(ID_ZOOM_IN, ID_ZOOM_OUT,
                "09-zoom-in.gif",
                "09-zoom-out.gif",
                "Q.F.UIEditor.zoom('" + getClientId(fc) + "')").encode();
    }

    // ---- Tools
    private abstract class Tool {

        protected String jsEvent;
        protected String tip;

        public Tool(String jsEvent, String tip) {
            this.jsEvent = jsEvent;
            this.tip = tip;
        }

        abstract void encode() throws IOException;
    }

    private class ImageTool extends Tool {

        protected String imgSrc;

        public ImageTool(String jsEvent, String tip, String imgSrc) {
            super(jsEvent, tip);
            this.imgSrc = imgSrc;
        } 

        public void encode() throws IOException {
            FacesContext fc = FacesContext.getCurrentInstance();
            ResponseWriter rw = fc.getResponseWriter();
            rw.startElement("div", Editor.this);
            rw.writeAttribute("style", "border:1px solid #F0F0F0;float:left;width:22px;height:22px;padding:1px;margin:1px;", null);
            rw.startElement("img", Editor.this);
            rw.writeAttribute("src", QFaces.getResLoader2().makeImageSrc(this.imgSrc), null);
            rw.writeAttribute("onclick", this.jsEvent, null);
            rw.writeAttribute("border", "0", null);
            rw.writeAttribute("title", this.tip, null);
            rw.endElement("img");
            rw.endElement("div");
        }
    }

    private class SelectTool extends Tool {

        private List<Item> items;

        public SelectTool(String jsEvent, String tip, List<Item> items) {
            super(jsEvent, tip);
            this.items = items;
        }

        public void encode() throws IOException {
            FacesContext fc = FacesContext.getCurrentInstance();
            ResponseWriter rw = fc.getResponseWriter();
            if (items == null) {
                throw new NullPointerException("Items not found!");
            }
            rw.startElement("div", Editor.this);
            rw.writeAttribute("style", "border:0px solid gray;float:left;height:22px;line-height:22px;padding:1px;margin:1px;", null);
            rw.startElement("select", Editor.this);
            rw.writeAttribute("style", "width:75px;", null);
            rw.writeAttribute("onchange", this.jsEvent, null);
            rw.writeAttribute("title", this.tip, null);
            for (Item item : this.items) {
                rw.startElement("option", Editor.this);
                rw.writeAttribute("value", item.getValue(), null);
                rw.writeText(item.getLabel(), null);
                rw.endElement("option");
            }
            rw.endElement("select");
            rw.endElement("div");
        }
    }

    private class FullScreenTool extends Tool {

        protected String imgSrc;
        protected String ID_ZOOM_IN;	// “放大”按钮的ID
        protected String ID_ZOOM_OUT;	// "缩小“按钮的ID
        protected String imgSrcZoomIn;
        protected String imgSrcZoomOut;

        public FullScreenTool(String ID_ZOOM_IN, String ID_ZOOM_OUT, String imgSrcZoomIn, String imgSrcZoomOut, String jsEvent) {
            super(jsEvent, null);
            this.ID_ZOOM_IN = ID_ZOOM_IN;
            this.ID_ZOOM_OUT = ID_ZOOM_OUT;
            this.imgSrcZoomIn = imgSrcZoomIn;
            this.imgSrcZoomOut = imgSrcZoomOut;
        }

        public void encode() throws IOException {
            FacesContext fc = FacesContext.getCurrentInstance();
            ResponseWriter rw = fc.getResponseWriter();
            rw.startElement("div", Editor.this);
            rw.writeAttribute("style", "border:1px solid #F0F0F0;float:left;width:22px;height:22px;padding:1px;margin:1px;", null);

            rw.startElement("img", Editor.this);
            rw.writeAttribute("id", this.ID_ZOOM_IN, null);
            rw.writeAttribute("name", this.ID_ZOOM_IN, null);
            rw.writeAttribute("src", QFaces.getResLoader2().makeImageSrc(this.imgSrcZoomIn), null);
            rw.writeAttribute("onclick", this.jsEvent, null);
            rw.writeAttribute("title", "放大", null);
            rw.writeAttribute("style", "margin:3px 0 0 3px;", null);
            rw.endElement("img");

            rw.startElement("img", Editor.this);
            rw.writeAttribute("id", this.ID_ZOOM_OUT, null);
            rw.writeAttribute("name", this.ID_ZOOM_OUT, null);
            rw.writeAttribute("src", QFaces.getResLoader2().makeImageSrc(this.imgSrcZoomOut), null);
            rw.writeAttribute("onclick", this.jsEvent, null);
            rw.writeAttribute("title", "缩小", null);
            rw.writeAttribute("style", "margin:3px 0 0 3px;", null);
            rw.endElement("img");
            rw.endElement("div");
        }
    }

    private List<Tool> initTools(FacesContext fc, String clientId) {
        List<Tool> _tools = new ArrayList<Tool>();

        _tools.add(new ImageTool("Q.F.UIEditor.doBold('" + clientId + "')", "加粗", "09-bold.png"));
        _tools.add(new ImageTool("Q.F.UIEditor.doItalic('" + clientId + "')", "斜体", "09-italic.png"));
        _tools.add(new ImageTool("Q.F.UIEditor.doUnderline('" + clientId + "')", "下划线", "09-underline.png"));
        _tools.add(new ImageTool("Q.F.UIEditor.doStrikeThrough('" + clientId + "')", "删除线", "09-strike.png"));

        _tools.add(new ImageTool("Q.F.UIEditor.doSubscript('" + clientId + "')", "下标", "09-subscript.png"));
        _tools.add(new ImageTool("Q.F.UIEditor.doSuperscript('" + clientId + "')", "上标", "09-superscript.png"));
        _tools.add(new ImageTool("Q.F.UIEditor.doForeColor('" + clientId + "', this.parentNode)", "前景色", "09-text-color.png"));
        _tools.add(new ImageTool("Q.F.UIEditor.doBackColor('" + clientId + "', this.parentNode)", "背景色", "09-background-color.png"));

        _tools.add(new ImageTool("Q.F.UIEditor.doJustifyLeft('" + clientId + "')", "左对齐", "09-justify-left.png"));
        _tools.add(new ImageTool("Q.F.UIEditor.doJustifyCenter('" + clientId + "')", "居中", "09-justify-center.png"));
        _tools.add(new ImageTool("Q.F.UIEditor.doJustifyRight('" + clientId + "')", "右对齐", "09-justify-right.png"));

        _tools.add(new ImageTool("Q.F.UIEditor.doOrderList('" + clientId + "')", "序号1", "09-ordered-list.png"));
        _tools.add(new ImageTool("Q.F.UIEditor.doUnOrderList('" + clientId + "')", "序号2", "09-unordered-list.png"));

        _tools.add(new ImageTool("Q.F.UIEditor.doIndent('" + clientId + "')", "内缩进", "09-indent.png"));
        _tools.add(new ImageTool("Q.F.UIEditor.doOutdent('" + clientId + "')", "外缩进", "09-outdent.png"));

        // select tools
        List<Item> nameList = new ArrayList<Item>();
        List<Item> sizeList = new ArrayList<Item>();
        String[] fonts = this.fontNameList.split(",");
        nameList.add(new Item("", "Font"));
        for (String font : fonts) {
            nameList.add(new Item(font.trim(), font.trim()));
        }
        String[] fontSizes = this.fontSizeList.split(",");
        sizeList.add(new Item("", "Size"));
        for (String fontSize : fontSizes) {
            sizeList.add(new Item(fontSize.trim(), fontSize.trim()));
        }
        SelectTool selFontName = new SelectTool("Q.F.UIEditor.doFontName('" + clientId + "', this)", "选择字体", nameList);
        SelectTool selFontSize = new SelectTool("Q.F.UIEditor.doFontSize('" + clientId + "', this)", "字体大小", sizeList);
        _tools.add(selFontName);
        _tools.add(selFontSize);
        _tools.add(new ImageTool("Q.F.UIEditor.doCreateLink('" + clientId + "', this.parentNode)", "创建超链接", "09-link.png"));
        _tools.add(new ImageTool("Q.F.UIEditor.doUnlink('" + clientId + "')", "删除超链接", "09-unlink.png"));
        _tools.add(new ImageTool("Q.F.UIEditor.doRemoveFormat('" + clientId + "')", "删除格式", "09-remove-format.png"));
        _tools.add(new ImageTool("Q.F.UIEditor.doInsertImage('" + clientId + "', this.parentNode)", "插入图片", "09-gif.gif"));
        _tools.add(new ImageTool("Q.F.UIEditor.doInsertFlash('" + clientId + "', this.parentNode)", "插入Flash", "09-flash.png"));

        // 如果没有开启“插入代码”的功能，则不需要泻染该功能
        if (this.enableInsertCode) {
            _tools.add(new ImageTool("Q.F.UIEditor.doInsertCode('" + clientId + "', this.parentNode)", "插入代码", "09-code.gif"));
        }
        if (!isIE(fc)) { // IE下Undo,Redo存在问题
            _tools.add(new ImageTool("Q.F.UIEditor.doUndo('" + clientId + "')", "后退", "09-undo.png"));
            _tools.add(new ImageTool("Q.F.UIEditor.doRedo('" + clientId + "')", "前进", "09-redo.png"));
        }

        return _tools;
    }

    private boolean isIE(FacesContext fc) {
        HttpServletRequest request = (HttpServletRequest) fc.getExternalContext().getRequest();
        String userAgent = request.getHeader("user-agent");
        return (userAgent != null && userAgent.indexOf("MSIE") != -1);
    }

    // ---- decode
    
    @Override
    public void decode(FacesContext fc) {
    	setValid(true);
    	
    	// 必须保持ID一致
    	this.ID_INPUT = this.getClientId(fc);
    	this.ID_MODE  = this.getClientId(fc) + ":mode";
    	
    	
        Map<String, String> map = fc.getExternalContext().getRequestParameterMap();
        String inputValue = map.get(this.ID_INPUT);
        String modeValue = map.get(this.ID_MODE);

        // 判断客户端用户当前所处的编辑模式
        if (modeValue != null) {
            this.mode = ("1".equals(modeValue.trim()) ? 1 : 0);
        }

        // Last modify: 20100620, remove
        // 下面代码存在bug,当editor没有放在form之内时，
        // 如果其它form提交，则editor会重置为html editor模式，即mode = 0.
        // 但是默认应该为mode = 1,即可视化模式, 代码已经在上面修正，
//        if (modeValue != null && modeValue.trim().equals("1")) {
//        	this.mode = 1;
//        } else {
//        	this.mode = 0;
//        }

        // 修正节点错误，在某些要求较严格的浏览器下，如:chrome, <br>可能造成错误而导致页面不能正常显示，必须转化为<br/>
        // 也就是所有节点必须有完整的结束标志
        inputValue = filterValue(inputValue);

        // 获取客户端submit的value并处理， 这里的value是未经过过滤的
        // 替换_value中的: <qfaces-editor-node id='id1,id2'><span style='border:1px solid gray;'>+</span></qfaces-editor-node>
        // 注意可能出现的长匹配的问题，即第一个<qfaces-editor-node 匹配到最后一个</qfaces-editor-node> 的问题
        String imgOpen = "<img src=\"" + QFaces.getResLoader2().makeImageSrc("09-node-closed.gif") + "\" style=\"vertical-align:middle;\"/>";
        String imgClose = "<img src=\"" + QFaces.getResLoader2().makeImageSrc("09-node-opened.gif") + "\" style=\"vertical-align:middle;\"/>";
        String nodeOpen = "<qfaces-node-editor>\\+</qfaces-node-editor>";
        String nodeClose = "<qfaces-node-editor>-</qfaces-node-editor>";

        inputValue = this.replace(inputValue, nodeOpen, imgOpen);
        inputValue = this.replace(inputValue, nodeClose, imgClose);

        this.setSubmittedValue(inputValue);
    }

    // 修正节点错误
    private String filterValue(String value) {
        if (value == null || "".equals(value)) {
            return "";
        }
        Pattern p;
        Matcher m;
        StringBuffer sb;
        // 过滤并修正一些节点问题，这些在一些较严格的浏览器上可能会造成显示不正常，如,chrome
        String[][] regx = {
            {"<br\\s*>", "<br />"}, // 大部分浏览器下换行符只显示为<br>（缺少结束标志）
            {"</img>", ""} // 修正图片节点，将可能存在的</img>移除，并在后面修正为<img ... />
        };
        for (int i = 0; i < regx.length; i++) {
            p = Pattern.compile(regx[i][0], Pattern.CASE_INSENSITIVE);
            m = p.matcher(value);
            sb = new StringBuffer();
            while (m.find()) {
                m.appendReplacement(sb, regx[i][1]);
                // 输出警告信息
                String temp = value.substring(m.start(), m.end());
                logger.warning("修正节点:" + temp + " -> " + regx[i][1]);
            }
            m.appendTail(sb);
            value = sb.toString();
        }
        // 修正图片节点问题，将 <img ....> 转换为 <img ... /> 在大部分浏览器下图片节点都缺少结束标志
        // <img.+?> ？号避免出现贪婪匹配
        String imgRegx = "<img.+?>";
        p = Pattern.compile(imgRegx, Pattern.CASE_INSENSITIVE);
        m = p.matcher(value);
        sb = new StringBuffer();
        while (m.find()) {
            int start = m.start();
            int end = m.end();
            // 找到匹配的倒数第二个字符,如果倒数第二个字符为 "/" ,
            // 则表示结束符正常 <img ... />,否则需要修正
            char letter = value.charAt(end - 2);
            if (letter != '/') {
                String replace = value.substring(start, (end - 1)) + "/>";
                m.appendReplacement(sb, replace);
                // 输出警告信息
                String temp = value.substring(m.start(), m.end());
                logger.warning("修正图片节点:" + temp + " -> " + replace);
            }
        }
        m.appendTail(sb);
        value = sb.toString();
        return value;
    }

    private String replace(String value, String source, String replace) {
        Pattern p = Pattern.compile(source, Pattern.CASE_INSENSITIVE);
        StringBuffer sb = new StringBuffer();
        Matcher m = p.matcher(value);
        while (m.find()) {
            m.appendReplacement(sb, replace);
        }
        m.appendTail(sb);
        return sb.toString();
    }
}
